#include "PadStackWidget.h"

#include "GraphicsItems.h"
#include "GraphicsPadStackElementItem.h"

#include "GraphicsArcOfCircle.h"
#include "GraphicsButterflyPrimitive.h"
#include "GraphicsCircularShapeItem.h"
#include "GraphicsRectangularShapeItem.h"
#include "GraphicsRegularPolygonPrimitive.h"
#include "GraphicsTrianglePrimitive.h"

#include "PlaceArcOfCircle.h"
#include "PlaceButterflyPrimitive.h"
#include "PlaceCircularPadTask.h"
#include "PlaceRectangularPadTask.h"
#include "PlaceRegularPolygonPrimitive.h"
#include "PlaceTrianglePrimitive.h"

#include "MainWindow.h"

#include "LeGraphicsView/LeGraphicsFeatureOption.h"
#include "LeGraphicsView/LeGraphicsItem.h"
#include "LeGraphicsView/LeGraphicsItemLayer.h"
#include "LeGraphicsView/LeGraphicsLayerStackModel.h"
#include "LeGraphicsView/LeGraphicsScene.h"
#include "LeGraphicsView/LeGraphicsView.h"
#include "LeGraphicsView/Solarized.h"

#include "LeDocumentObject/Document.h"
#include "LeDocumentObject/DocumentObjectInspector.h"

#include "LeIpc7351/PadStack.h"
#include "LeIpc7351/PadStackElement.h"
#include "LeIpc7351/Primitives/ArcOfCircle.h"
#include "LeIpc7351/Primitives/ButterflyPrimitive.h"
#include "LeIpc7351/Primitives/CirclePrimitive.h"
#include "LeIpc7351/Primitives/RectanglePrimitive.h"
#include "LeIpc7351/Primitives/RegularPolygonPrimitive.h"
#include "LeIpc7351/Primitives/TrianglePrimitive.h"

#include <QDebug>
#include <QGridLayout>
#include <QHeaderView>
#include <QTableView>
#include <QToolBar>


PadStackWidget::PadStackWidget(QWidget *parent)
    : EditorWidget(parent)
    , LDO::IDocumentObjectListener()
{
    setupScene();
    setupView();
    createTasks();
    setupToolBar();
    setupLayerBar();
    setupLayout();
}

LeGraphicsScene *PadStackWidget::scene() const
{
    return m_scene;
}

LeGraphicsView *PadStackWidget::view() const
{
    return m_view;
}

PadStackElement *PadStackWidget::currentElement() const
{
    auto layer = m_scene->currentLayer();
    Q_ASSERT(layer != nullptr);
    Q_ASSERT(m_layerToElement.contains(layer));
    return m_layerToElement.value(layer);
}

QString PadStackWidget::title() const
{
    return "Untitled";
}

void PadStackWidget::restoreGeometryAndState(QSettings &settings)
{
    Q_UNUSED(settings);
}

void PadStackWidget::saveGeometryAndState(QSettings &settings)
{
    Q_UNUSED(settings);
}

void PadStackWidget::setupScene()
{
    m_scene = new LeGraphicsScene(this);
    m_scene->setObjectName("scene");

    LeGraphicsPalette palette;
    palette.setBrush(LeGraphicsPalette::Background, Solarized::backgroundHighlight);
    palette.setBrush(LeGraphicsPalette::Grid, Solarized::secondaryContent);
    palette.setBrush(LeGraphicsPalette::Origin, Solarized::foreground);
    palette.setBrush(LeGraphicsPalette::Cursor, Solarized::foreground);
    palette.setBrush(LeGraphicsPalette::Selection, Solarized::foregroundHighlight);
    palette.setBrush(LeGraphicsPalette::Highlight, Solarized::foreground);
    palette.setBrush(LeGraphicsPalette::Handle, Solarized::foreground);
    palette.setBrush(LeGraphicsPalette::RubberBand, Solarized::foregroundHighlight);
    palette.setBrush(LeGraphicsPalette::BadMark, Solarized::red);
    palette.setBrush(LeGraphicsPalette::GoodMark, Solarized::green);
    //palette.setBrush(LeGraphicsPalette::Feature, Solarized::primaryContent);
    m_scene->setGraphicsPalette(palette);

    auto featureOption = LeGraphicsFeatureOption();
    featureOption.setAllFeatureVisible(true);
    featureOption.setAllFeatureBrush(QBrush(Solarized::foreground, Qt::SolidPattern));
    m_scene->foregroundLayer()->setFeatureOption(featureOption);
    m_scene->foregroundLayer()->setVisible(true);
}

LeGraphicsItemLayer* PadStackWidget::addLayer(PadStackElement *element,
                                              const QString &fullName,
                                              const QString &shortName,
                                              const QString &tinyName,
                                              const QColor &color)
{
    auto layer = new LeGraphicsItemLayer();
    layer->setNames(fullName, shortName, tinyName);
    auto featureOption = LeGraphicsFeatureOption();
    featureOption.setAllFeatureVisible(true);
    featureOption.setAllFeatureBrush(QBrush(color, Qt::SolidPattern));
    layer->setFeatureOption(featureOption);
    layer->setVisible(true);

    element->setObjectUserName(layer->fullName());

    m_layerToElement.insert(layer, element);
    m_elementToLayer.insert(element, layer);

    return layer;
}

void PadStackWidget::setupView()
{
    // cppcheck-suppress publicAllocationError
    m_view = new LeGraphicsView();
    m_view->setObjectName("view");
    m_view->setScene(m_scene);
    m_view->setInteractionMode(LeGraphicsView::MultipleSelectionMode);
    m_view->setOrientation(LeGraphicsView::XRightYUp);
    m_view->setFrameStyle(QFrame::NoFrame);
    m_view->layout()->setMargin(0);
    m_view->layout()->setSpacing(0);
}

void PadStackWidget::setPadStack(PadStack *padStack)
{
    m_scene->clearLayers();
    m_layerToElement.clear();

    m_padStack = padStack;

    if (m_padStack == nullptr)
        return;

    for (auto element: padStack->elements())
    {
        LeGraphicsItemLayer *layer = nullptr;
        switch (element->function()) {
            case L7::MountedLand:
                layer = addLayer(element,
                         "Mounted copper",
                         "Mnt Cu",
                         "MC",
                         Solarized::red);
                break;
            case L7::OppositeLand:
                layer = addLayer(element,
                         "Opposite copper",
                         "Opp Cu",
                         "OC",
                         Solarized::red);
                break;
            case L7::MountedSolderMask:
                layer = addLayer(element,
                         "Mounted solder mask",
                         "Mnt mask",
                         "MM",
                         Solarized::magenta);
                break;
            case L7::OppositeSolderMask:
                layer = addLayer(element,
                         "Opposite solder mask",
                         "Opp mask",
                         "OM",
                         Solarized::magenta);
                break;
            case L7::MountedSolderPaste:
                layer = addLayer(element,
                         "Mounted solder paste",
                         "Mnt paste",
                         "MP",
                         Solarized::violet);
                break;
            case L7::OppositeSolderPaste:
                layer = addLayer(element,
                         "Opposite solder paste",
                         "Opp paste",
                         "OP",
                         Solarized::violet);
                break;
            case L7::MountedAssembly:
                layer = addLayer(element,
                         "Mounted assembly",
                         "Mnt assy",
                         "MA",
                         Solarized::yellow);
                break;
            case L7::OppositeAssembly:
                layer = addLayer(element,
                         "Opposite assembly",
                         "Opp assy",
                         "OA",
                         Solarized::yellow);
                break;
            case L7::InternalLand:
                layer = addLayer(element,
                         "Internal copper",
                         "Int Cu",
                         "IC",
                         Solarized::red);
                break;
            case L7::PlaneLand:
                layer = addLayer(element,
                         "Plane copper",
                         "Pln Cu",
                         "PC",
                         Solarized::red);
                break;
            case L7::ThermalReliefLand:
                layer = addLayer(element,
                         "Thermal copper",
                         "Thm Cu",
                         "TC",
                         Solarized::red);
                break;
            case L7::Hole:
                layer = addLayer(element,
                         "Through hole",
                         "Hole",
                         "TH",
                         Solarized::blue);
                break;
            case L7::KeepOut:
                layer = addLayer(element, // FIXME: Mounted and opposite
                         "Mounted keepout",
                         "Mnt keepout",
                         "MK",
                         Solarized::green);
                break;
            case L7::UnknownElementFunction:
            default:
                qWarning() << "PadStackWidget::setPadStack: Unhandled element function: " << element->function();
                continue;
        }

        Q_UNUSED(layer)
        beginListeningToDocumentObject(element);
        addShapeItem(element);
    }

    m_scene->setLayers(m_layerToElement.keys());
}

void PadStackWidget::createTasks()
{
    m_placeTasks.append(new PlaceCircularPadTask(this));
    m_placeTasks.append(new PlaceRectangularPadTask(this));
    m_placeTasks.append(new PlaceTrianglePrimitiveTask(this));
    m_placeTasks.append(new PlaceRegularPolygonPrimitiveTask(this));
    m_placeTasks.append(new PlaceArcOfCircleTask(this));
    m_placeTasks.append(new PlaceButterflyPrimitiveTask(this));
    for (auto task: m_placeTasks)
    {
        connect(task, &AbstractTask::started,
                this, &PadStackWidget::taskStarted);
        connect(task, &AbstractTask::accepted,
                this, &PadStackWidget::taskFinished);
        connect(task, &AbstractTask::rejected,
                this, &PadStackWidget::taskFinished);
        connect(task->action(), &QAction::triggered,
                this, &PadStackWidget::taskActionTriggered);
        task->action()->setData(QVariant::fromValue<AbstractTask*>(task));
    }
}


void PadStackWidget::setupLayerBar()
{
    // cppcheck-suppress publicAllocationError
    m_layerStackView =  new QTableView();
    m_layerStackModel = new LeGraphicsLayerStackModel(this);
    m_layerStackModel->setScene(m_scene);
    m_layerStackView->setModel(m_layerStackModel);
    m_layerStackView->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_layerStackView->setSelectionMode(QAbstractItemView::SingleSelection);
    m_layerStackView->horizontalHeader()->setStretchLastSection(true);
    m_layerStackView->horizontalHeader()->hide();
    m_layerStackView->setShowGrid(false);
    m_layerStackView->setAlternatingRowColors(false);
    for (int i=0; i<m_layerStackModel->columnCount(); i++)
        m_layerStackView->hideColumn(i);
    m_layerStackView->showColumn(m_layerStackModel->CompactColumn);
    m_layerStackView->setFixedWidth(48);

    m_layerStackView->setStyleSheet(QString("QTableView { "
                                            " background: %1;"
                                            " selection-background-color: %2;"
                                            " spacing: 0;"
                                            " padding: 0;"
                                            " border: 0;"
                                            "}"
                                            "QTableView::indicator:checked {"
                                            " image: url(:/icons/visibility-on.svg);"
                                            "}"
                                            "QTableView::indicator:unchecked {"
                                            " image: url(:/icons/visibility-off.svg);"
                                            "}"
                                            "QTableView QTableCornerButton::section {"
                                            " background: %1;"
                                            "}"
                                            )
                                    .arg(Solarized::background.name(),
                                         Solarized::backgroundHighlight.name()));
    connect(m_layerStackView->selectionModel(), &QItemSelectionModel::currentRowChanged,
            this, [this](const QModelIndex &current, const QModelIndex &/*previous*/)
    {
        LeGraphicsItemLayer *layer = m_layerStackModel->graphicsLayer(current);
        m_scene->setCurrentLayer(layer);
    });
    connect(m_scene, &LeGraphicsScene::currentLayerChanged,
            this, [this](LeGraphicsItemLayer *layer) {
        m_layerStackView->selectRow(m_scene->featureLayers().indexOf(layer));
    });
}

void PadStackWidget::taskActionTriggered()
{
    auto action = qobject_cast<QAction*>(sender());
    auto task = action->data().value<PadStackTask*>();
    task->start();
}

void PadStackWidget::applyDocumentSelection(const QList<LDO::IDocumentObject *> &current, const QList<LDO::IDocumentObject *> &previous)
{
    QSet<QGraphicsItem*> previousSceneItems;
    for (LDO::IDocumentObject *object: previous)
    {
        if (!qobject_cast<Primitive*>(object))
            continue;
        auto primitive = qobject_cast<Primitive*>(object);
        previousSceneItems.insert(m_documentPrimitiveToSceneItem.value(primitive));
    }

    QSet<QGraphicsItem*> currentSceneItems;
    for (LDO::IDocumentObject *object: current)
    {
        if (!qobject_cast<Primitive*>(object))
            continue;
        auto primitive = qobject_cast<Primitive*>(object);
        currentSceneItems.insert(m_documentPrimitiveToSceneItem.value(primitive));
    }

    QSet<QGraphicsItem*> selectedItems = m_scene->selectedItems().toSet();
    if (currentSceneItems == selectedItems)
        return;

    QSet<QGraphicsItem*> toDeselect = selectedItems;
    toDeselect.subtract(currentSceneItems);
    for (auto item: toDeselect)
        item->setSelected(false);

    QSet<QGraphicsItem*> toSelect = currentSceneItems;
    toSelect.subtract(selectedItems);
    for (auto item: toSelect)
    {
        m_scene->setCurrentLayer(static_cast<LeGraphicsItem*>(item)->layer());
        item->setSelected(true);
    }
}

void PadStackWidget::applySceneSelection()
{
    QList<LDO::IDocumentObject*> selectedDocumentObjects;
    for (auto item: m_scene->selectedItems())
    {
        if (!m_sceneItemToDocumentPrimitive.contains(item))
            continue;
        auto primitive = m_sceneItemToDocumentPrimitive.value(item);
        selectedDocumentObjects.append(primitive);
    }
    m_document->setSelectedObjects(selectedDocumentObjects);
}

void PadStackWidget::addShapeItem(PadStackElement *element)
{
    if (element->primitive() == nullptr)
        return;

    auto layer = m_elementToLayer.value(element, nullptr);
    Q_ASSERT(layer != nullptr);

    if (auto circle = qobject_cast<CirclePrimitive*>(element->primitive()))
    {
        auto item = new GraphicsCircularShapeItem(circle);
        layer->addToLayer(item);
        m_documentPrimitiveToSceneItem.insert(circle, item);
        m_sceneItemToDocumentPrimitive.insert(item, circle);

    }
    else if (auto rect = qobject_cast<RectanglePrimitive*>(element->primitive()))
    {
        auto item = new GraphicsRectangleShapeItem(rect);
        layer->addToLayer(item);
        m_documentPrimitiveToSceneItem.insert(rect, item);
        m_sceneItemToDocumentPrimitive.insert(item, rect);
    }
    else if (auto triangle = qobject_cast<TrianglePrimitive*>(element->primitive()))
    {
        auto item = new GraphicsTrianglePrimitive(triangle);
        layer->addToLayer(item);
        m_documentPrimitiveToSceneItem.insert(triangle, item);
        m_sceneItemToDocumentPrimitive.insert(item, triangle);
    }
    else if (auto butterfly = qobject_cast<ButterflyPrimitive*>(element->primitive()))
    {
        auto item = new GraphicsButterflyPrimitive(butterfly);
        layer->addToLayer(item);
        m_documentPrimitiveToSceneItem.insert(butterfly, item);
        m_sceneItemToDocumentPrimitive.insert(item, butterfly);
    }
    else if (auto regularPolygon = qobject_cast<RegularPolygonPrimitive*>(element->primitive()))
    {
        auto item = new GraphicsRegularPolygonPrimitive(regularPolygon);
        layer->addToLayer(item);
        m_documentPrimitiveToSceneItem.insert(regularPolygon, item);
        m_sceneItemToDocumentPrimitive.insert(item, regularPolygon);
    }
    else if (auto arcOfCircle = qobject_cast<ArcOfCircle*>(element->primitive()))
    {
        auto item = new GraphicsArcOfCircle(arcOfCircle);
        layer->addToLayer(item);
        m_documentPrimitiveToSceneItem.insert(arcOfCircle, item);
        m_sceneItemToDocumentPrimitive.insert(item, arcOfCircle);
    }
    else
    {
        qWarning() << "PadStackWidget::setPadStack: Unhandled primitive type: " << element->primitive();
    }
}

void PadStackWidget::removeShapeItem(PadStackElement *element)
{
    if (element->primitive() == nullptr)
        return;

    auto layer = m_elementToLayer.value(element, nullptr);
    Q_ASSERT(layer != nullptr);
    auto sceneItem = m_documentPrimitiveToSceneItem.value(element->primitive(), nullptr);
    Q_ASSERT(sceneItem != nullptr);
    m_documentPrimitiveToSceneItem.remove(element->primitive());
    m_sceneItemToDocumentPrimitive.remove(sceneItem);
}

void PadStackWidget::setupLayout()
{
    auto cornerWidget = new QWidget();
    auto gridLayout = new QGridLayout();
    gridLayout->addWidget(m_toolBar, 0, 0);
    gridLayout->addWidget(cornerWidget, 0, 1);
    gridLayout->addWidget(m_view, 1, 0);
    gridLayout->addWidget(m_layerStackView, 1, 1);
    gridLayout->setSpacing(0);
    gridLayout->setMargin(0);
    setLayout(gridLayout);
    cornerWidget->setStyleSheet(QString("QWidget { "
                                        " background: %1;"
                                        "}"
                                        )
                                .arg(Solarized::background.name()));
}

void PadStackWidget::documentObjectAboutToBeInserted(LDO::IDocumentObject *parent,
                                                     LDO::IDocumentObject *child,
                                                     int index)
{
    Q_UNUSED(parent)
    Q_UNUSED(child)
    Q_UNUSED(index)
}

void PadStackWidget::documentObjectInserted(LDO::IDocumentObject *parent,
                                            LDO::IDocumentObject *child,
                                            int index)
{
    Q_UNUSED(parent)
    Q_UNUSED(child)
    Q_UNUSED(index)

}

void PadStackWidget::documentObjectAboutToBeRemoved(LDO::IDocumentObject *parent,
                                                    LDO::IDocumentObject *child,
                                                    int index)
{
    Q_UNUSED(parent)
    Q_UNUSED(child)
    Q_UNUSED(index)
    if (child == m_padStack)
    {
        // clear padstack/widget
    }
    if (parent == m_padStack)
    {
        // remove element
    }
    for (const PadStackElement *element: m_padStack->elements())
    {
        if (parent == element)
        {
            // remove primitive
        }
    }
}

void PadStackWidget::documentObjectRemoved(LDO::IDocumentObject *parent,
                                           LDO::IDocumentObject *child,
                                           int index)
{
    Q_UNUSED(parent)
    Q_UNUSED(child)
    Q_UNUSED(index)
}

void PadStackWidget::documentObjectAboutToChangeProperty(const LDO::IDocumentObject *object, const QString &name, const QVariant &value)
{
    Q_UNUSED(object)
    Q_UNUSED(name)
    Q_UNUSED(value)

    auto element = qobject_cast<const PadStackElement*>(object);
    if (element == nullptr)
        return;
    if (name != "primitive")
        return;
    removeShapeItem(const_cast<PadStackElement*>(element));
}

void PadStackWidget::documentObjectPropertyChanged(const LDO::IDocumentObject *object, const QString &name, const QVariant &value)
{
    Q_UNUSED(object)
    Q_UNUSED(name)
    Q_UNUSED(value)

    auto element = qobject_cast<const PadStackElement*>(object);
    if (element == nullptr)
        return;
    if (name != "primitive")
        return;
    addShapeItem(const_cast<PadStackElement*>(element));
}


void PadStackWidget::edit(MainWindow *gui, LDO::Document *document, LDO::IDocumentObject *object)
{
    m_gui = gui;
    if (m_document)
    {
        m_document->disconnect(this);
        m_scene->disconnect(this);
        m_gui->objectInspector()->disconnect(this);
    }

    m_document = document;
    beginListeningToDocumentObject(document);
    setPadStack(qobject_cast<PadStack*>(object));
    m_gui->objectInspector()->setDocumentObject(document, m_padStack);

    if (m_document != nullptr)
    {
        applyDocumentSelection(m_document->selectedObjects(), QList<LDO::IDocumentObject*>());
        connect(m_document, &LDO::Document::selectedObjectsChanged,
                this, &PadStackWidget::applyDocumentSelection);
        connect(m_scene, &LeGraphicsScene::selectionChanged,
                this, &PadStackWidget::applySceneSelection);
        //        connect(m_gui->objectInspector(), &DocumentObjectInspector::currentObjectChanged,
        //                this, )
    }
}


LDO::IDocumentObject *PadStackWidget::object() const
{
    return m_padStack;
}

LDO::Document *PadStackWidget::document() const
{
    return m_document;
}


QList<AbstractTask *> PadStackWidget::tasks() const
{
    return m_placeTasks;
}


void PadStackWidget::setupToolBar()
{
    //    m_toolBar = new QToolBar("Pad-stack tool bar");
    //    m_toolBar->setObjectName("padStackToolBar");
    //    m_toolBar->addAction(Gui::icon("primitive-shape-circle"), "Circular shape",
    //                         this, &PadStackWidget::placeCircle);
    //    m_toolBar->addAction(Gui::icon("primitive-shape-ellipse"), "Ellipsoidal shape");
    //    m_toolBar->addAction(Gui::icon("primitive-shape-rectangle"), "Rectangular shape");
    //    m_toolBar->addAction(Gui::icon("primitive-shape-dee"), "Dee shape");
    //    m_toolBar->addAction(Gui::icon("primitive-shape-oblong"), "Oblong shape");
    //    m_toolBar->addAction(Gui::icon("primitive-shape-regular-polygon"), "Regular polygon shape");
    //    m_toolBar->addAction(Gui::icon("primitive-outline-circle"), "Circle");
    //    m_toolBar->addAction(Gui::icon("primitive-outline-ellipse"), "Ellipse");
    //    m_toolBar->addAction(Gui::icon("primitive-outline-rectangle"), "Rectangle");
    //    m_toolBar->addAction(Gui::icon("primitive-outline-polyline"), "Polyline");
    //    m_toolBar->addAction(Gui::icon("primitive-outline-regular-polygon"), "Regular polygon");
    //    m_toolBar->addAction(Gui::icon("primitive-special-butterfly"), "Butterfly pattern");
    //    m_toolBar->addAction(Gui::icon("primitive-special-moire"), "Moiré pattern");
    //    m_toolBar->addAction(Gui::icon("primitive-special-thermal"), "Thermal pattern");

    //    auto group = new QActionGroup(this);
    //    group->setExclusive(true);
    //    for (auto action: m_toolBar->actions())
    //    {
    //        action->setCheckable(true);
    //        action->setChecked(false);
    //        group->addAction(action);
    //    }

    //    m_toolBar->layout()->setMargin(0);
    //    m_toolBar->layout()->setSpacing(0);
    //    m_toolBar->setFloatable(false);
    //    m_toolBar->setStyleSheet(QString("QToolBar { "
    //                                     " background: %1;"
    //                                     " spacing: 0;"
    //                                     "}"
    //                                     "QPushButton {"
    //                                     "}"
    //                                     "QToolButton:hover {"
    //                                     " background-color: %2;"
    //                                     "}"
    //                                     "QToolButton:checked {"
    //                                     " background-color: %2;"
    //                                     "}"
    //                                     )
    //                             .arg(Solarized::background.name(), Solarized::backgroundHighlight.name()));
}
